﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using FastMember;
using Platform.Storage.Entity;
using Platform.ObjectModel;
using Platform.ObjectModel.Design;
using Platform.Storage.Impl;
using System.Diagnostics;

namespace Platform.Linq
{

	/// <summary>
	/// Realiza a iteração e materialização das entidades baseada nos registros campo e valor
	/// </summary>
	/// <typeparam name="T"></typeparam>
	internal class DynamicQueryEnumerable<T> : IEnumerable<T>
	{

		public DynamicQueryEnumerable( EntityDataObjectMaterializer materializer, IEnumerable<FieldValue> fields, RelationshipEnumerator relations )
		{
			this.Fields = fields;
			this.Materializer = materializer;
			this.Relations = relations;
		}

		public IEnumerable<FieldValue> Fields
		{ get; private set; }

		public EntityDataObjectMaterializer Materializer
		{ get; private set; }

		public RelationshipEnumerator Relations
		{ get; private set; } 

		#region IEnumerable<T> Members

		public IEnumerator<T> GetEnumerator()
		{
			return new DynamicQueryEnumerable<T>.Enumerator( this.Materializer, this.Fields.GetEnumerator(), this.Relations );
		}

		#endregion

		#region IEnumerable Members

		System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
		{
			return this.GetEnumerator();
		}

		#endregion

		#region [ class Enumerator ]

		/// <summary>
		/// Percorre todos os campos e valores selecionados, montando o documento
		/// </summary>
		private sealed class Enumerator : IEnumerator<T>
		{
			private static readonly IComparer<string> TokenIdComparer = StringComparer.InvariantCultureIgnoreCase;

			private EntityDataObjectMaterializer materializer;	// Realiza a materialização do documento
			private IEnumerator<FieldValue> fields;			// Campos e valores dos documentos selecionados
			private RelationshipEnumerator relations;		// Documentos relacionados
			private bool initialized = false;				// Indica se o iterator foi inicializado
			private bool hasMoreDocs = false;				// Indica se há mais documentos para ser percorrido pelo iterator
			private bool hasMoreRelations = false;			// Indica se há mais relacionamento
			private T current;								// Documento atual

			

			/// <summary>
			/// 
			/// </summary>
			/// <param name="materializer"></param>
			/// <param name="fields"></param>
			public Enumerator( EntityDataObjectMaterializer materializer, IEnumerator<FieldValue> fields, RelationshipEnumerator relations )
			{
				Debug.Assert( materializer != null );
				Debug.Assert( fields != null );
				Debug.Assert( relations != null );

				this.fields = fields;
				this.materializer = materializer;
				this.relations = relations;
				//this.hasValue = false;
			}

			#region IEnumerator<T> Members

			public T Current
			{
				get { return this.current; }
			}

			#endregion

			#region IDisposable Members

			public void Dispose()
			{
				this.fields.Dispose();

				// Não deve nenhum documento filho pré-carregado
				Debug.Assert( this.relations.PreLoadedDocuments.Count == 0, "Nem todos os documentos filhos foram carregados." );
			}

			#endregion

			#region IEnumerator Members

			object System.Collections.IEnumerator.Current
			{
				get { return this.Current; }
			}

			
			/// <summary>
			/// Move para a primeira instância
			/// </summary>
			/// <returns></returns>
			private bool FirstInstance()
			{
				try
				{
					return this.NextInstance();
				}
				finally
				{
					this.initialized = true;
				}
			}

			/// <summary>
			/// Move para a próxima instância
			/// </summary>
			/// <returns></returns>
			private bool NextInstance()
			{
				// Se é o primeiro item, então movimento para o primeiro valor
				if ( this.initialized )
				{
					this.current = default( T );
				}
				else
				{
					this.hasMoreDocs = this.fields.MoveNext();
					this.hasMoreRelations = this.relations != null && this.relations.MoveNext();
				}
				
				// Cria a nova instância, somente se tiver campos para processar
				if ( this.hasMoreDocs )
				{
					var builder = this.materializer.CreateBuilder<T>();
					string instanceID = this.fields.Current.ReversedTokenId;
					int comparisonResult = 0;

					// Enquanto for a tupla do mesmo documento, continua materializando os campos
					while ( this.hasMoreDocs
						&& instanceID == this.fields.Current.ReversedTokenId )
					{
						// O cursor já está posicionado do campo atual
						builder.SetField( this.fields.Current );

						this.hasMoreDocs = this.fields.MoveNext();
					}
					this.current = ( T )( object )builder.GetDocument();

					// Preenche os relacionamentos
					while ( this.hasMoreRelations
						// Compara cada token, do menor para o maior, verificando se tem relacionamentos
						&& ( comparisonResult = TokenIdComparer.Compare( instanceID, this.relations.CurrentRelationship.ParentReversedTokenId )) >= 0 )
					{
						// se é o ID de outro documento, 
						// faz a materialização agora, mas coloca em um dicionário
						if ( comparisonResult == 0 )
						{
							builder.SetRelation( this.relations.CurrentRelationship, this.relations.Current );
						}
						else
						{
							// Adiciona em um dicionário, pois deve estar no próximo registro o documento pai
							this.relations.PreLoad();
						}

						this.hasMoreRelations = this.relations.MoveNext();
					}

					// Verifica se a relação não foi carregada anteriormente
					var preloaded = this.relations.PreLoadedDocuments;
					int index = 0;

					// Do maior para o menor, procura pelo relacionamento se já foi carregado anteriormente
					// na chamada do "PreLoad()"
					while ( index < preloaded.Count
						&& ( comparisonResult = TokenIdComparer.Compare( instanceID, preloaded[ index ].Item1.ParentReversedTokenId ) ) <= 0 )
					{
						// Se os dois tokens são iguais, então o documento
						// do relacionamento deve ser carregado e está na ordem reversa
						if ( comparisonResult == 0 )
						{
							var pair = preloaded[ index ];
							builder.SetRelation( pair.Item1, pair.Item2 );
							// Remove a tupla, pois já foi encontrado o pai
							preloaded.RemoveAt( index );
						}
						else
						{
							index++;
						}
					}

					// Preenche os relacionamentos 
					// 1. Os relacionamentos estão ordenados pelo TokenID do pai e pela propriedade do pai que faz a relação
					// 2. Percorre o iterator:
					//	2.1. Se o campo tem o mesmo token, então monta os documentos filhos. Pode criar uma instância desse enumerator
					//  2.2. Senão percorre o enumetor montando o documento e armazenando em um dicionário, até encontrar o token maior que o token do pai

					return true;
				}

				// Retorna indicando se há mais objetos para serem retornados
				return this.hasMoreDocs;
			}

			public bool MoveNext()
			{
				if ( !this.initialized )
					return this.FirstInstance();
				else //if ( this.hasMore )	// Se possui mais fields para serem processados
					return this.NextInstance();
				//else
				//    throw new InvalidOperationException( "Já acabou as lista de campos para serem processados." );
			}

			public void Reset()
			{
				this.fields.Reset();
				this.current = default( T );
				this.hasMoreDocs = false;
				this.initialized = false;
			}

			#endregion
		}

		#endregion 

		
	}


	/// <summary>
	/// 
	/// </summary>
	public sealed class RelationshipEnumerator : IEnumerator<DocumentFlake>
	{

		//private Schema parentSchema = null;
		private IEnumerator<RelationPair> relatedDocs = null;
		//private DocumentFlake currentDoc;
		//private DataObjectRelationship currentRelationship;
		private EntityDataObjectMaterializer materializer;
		//private LibrarySchemaWorkspace workspace;
		private bool isMoving = false;
		private bool hasMore = false;
		private List<Tuple<DataObjectRelationship, DocumentFlake>> loaded = new List<Tuple<DataObjectRelationship, DocumentFlake>>();


		internal RelationshipEnumerator()
			: this( true )
		{ }

		private RelationshipEnumerator( bool isEmpty )
		{
			if ( isEmpty )
			{
				this.isMoving = true;
				this.hasMore = false;
				this.relatedDocs = Enumerable.Empty<RelationPair>().GetEnumerator();
			}
		}

		internal static RelationshipEnumerator Create( EntityDataObjectMaterializer materializer, Type entityType, IQueryResolver resolver, IQueryable<FieldValue> parentFields )
		{
			RelationshipEnumerator enumerator = new RelationshipEnumerator( false );

			// Monta a consulta para retornar os relacionamentos aqui nesse ponto
			IQueryable<DataObjectRelationship> relations = resolver.Relations;
			IQueryable<FieldValue> childValues = resolver.Fields;

			Schema schema;
			if ( resolver.TryGetSchema( entityType, out schema ) )
			{
				//enumerator.parentSchema = schema;
				string keyField = schema.QualifiedName.FieldNameWithSchema( schema.FieldKey ).ToLowerInvariant();
				string schemaName = schema.QualifiedName.FullName.ToLowerInvariant();

				var query = ( from relation in relations
							  join childValue in childValues on relation.ChildReversedTokenId equals childValue.ReversedTokenId
							  where relation.ParentLoweredFieldName.StartsWith( schemaName )
								 && parentFields.Where( pf => pf.LoweredFieldName == keyField ).Any( pf => pf.ReversedTokenId == relation.ParentReversedTokenId )
							  orderby relation.ParentReversedTokenId, relation.ParentLoweredFieldName, relation.Order
							  select new RelationPair()
							  {
								  Relationship = relation,
								  Field = childValue
							  } );

				enumerator.relatedDocs = query.AsEnumerable().GetEnumerator();
			}


			return enumerator;
		}

		/// <summary>
		/// Obtém ou define o relacionamento atual
		/// </summary>
		public DataObjectRelationship CurrentRelationship
		{ get; private set; }

		/// <summary>
		/// Obtém a lista de documentos que formam marcados como pré-carregados
		/// </summary>
		public IList<Tuple<DataObjectRelationship, DocumentFlake>> PreLoadedDocuments
		{
			get { return this.loaded; }
		}

		/// <summary>
		/// Obtém o tipo do documento filho do relacionamento
		/// </summary>
		/// <returns></returns>
		private Type GetCurrentChildDocType()
		{
			return this.materializer.GetDocumentType( DataObjectName.Parse( this.CurrentRelationship.ChildLoweredName ) );
			//return this.workspace.GetDocType( this.CurrentRelationship.ChildLoweredName );
		}

		/// <summary>
		/// Pré-carrega o a tupla relação e documento filho
		/// </summary>
		internal void PreLoad()
		{
			// Adiciona na frente, para ficar fácil percorrer a lista se precisar ordenar quando está carregando 
			// cada relacionamento em ordem
			this.loaded.Insert( 0, new Tuple<DataObjectRelationship, DocumentFlake>( this.CurrentRelationship, this.Current ) );
			//this.loaded.Add( new Tuple<DataObjectRelationship, DocumentFlake>( this.CurrentRelationship, this.Current ) );
		}

		#region IEnumerator<DocumentFlake> Members

		/// <summary>
		/// Obtém ou define o documento atual
		/// </summary>
		public DocumentFlake Current
		{
			get;
			private set;
		}

		#endregion

		#region IDisposable Members

		public void Dispose()
		{
			this.relatedDocs.Dispose();
		}

		#endregion

		#region IEnumerator Members

		object System.Collections.IEnumerator.Current
		{
			get { return this.Current; }
		}

		public bool MoveNext()
		{
			// Se está iniciando a percorrer os dados
			if ( this.isMoving )
			{
				this.CurrentRelationship = default( DataObjectRelationship );
				this.Current = default( DocumentFlake );
			}
			else
			{
				this.hasMore = this.relatedDocs.MoveNext();
				this.isMoving = true;
			}

			// Se ainda tem campos e valores para materializar
			if ( this.hasMore )
			{
				// Indica quem está sendo materializado no momento
				string instanceID = this.relatedDocs.Current.Field.ReversedTokenId;
				IDataObjectBuilder builder = this.materializer.CreateBuilder( this.GetCurrentChildDocType() );

				// Relacionamento atual que está sendo materializado
				this.CurrentRelationship = this.relatedDocs.Current.Relationship;

				// Documento relacionado que será materializado
				while ( this.hasMore
					&& instanceID == this.relatedDocs.Current.Field.ReversedTokenId )
				{
					builder.SetField( this.relatedDocs.Current.Field );
					this.hasMore = this.relatedDocs.MoveNext();
				}

				// Define o documento que foi construído
				this.Current = ( DocumentFlake )builder.GetDocument();
				return true;
			}
			else
			{
				return false;
			}
		}

		public void Reset()
		{
			this.relatedDocs.Reset();
			this.isMoving = false;
			this.hasMore = false;
			this.Current = default( DocumentFlake );
			this.CurrentRelationship = default( DataObjectRelationship );
		}

		#endregion

		#region [ struct RelationPair ]

		/// <summary>
		/// 
		/// </summary>
		public struct RelationPair
		{
			/// <summary>
			/// 
			/// </summary>
			public DataObjectRelationship Relationship
			{ get; set; }

			/// <summary>
			/// 
			/// </summary>
			public FieldValue Field
			{ get; set; }
		}

		#endregion

	}

}
